package com.shm.leetcode;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
 * @author: shm
 * @dateTime: 2020/10/18 9:45
 * @description: 19. 删除链表的倒数第N个节点
 * 给定一个链表，删除链表的倒数第 n 个节点，并且返回链表的头结点。
 *
 * 示例：
 *
 * 给定一个链表: 1->2->3->4->5, 和 n = 2.
 *
 * 当删除了倒数第二个节点后，链表变为 1->2->3->5.
 * 说明：
 *
 * 给定的 n 保证是有效的。
 *
 * 进阶：
 *
 * 你能尝试使用一趟扫描实现吗？
 */
public class RemoveNthFromEnd {
    /**
     * @author: shm
     * @dateTime: 2020/10/18 9:33
     * @description: 前言
     * 在对链表进行操作时，一种常用的技巧是添加一个哑节点（dummy node），它的 \textit{next}next 指针指向链表的头节点。这样一来，我们就不需要对头节点进行特殊的判断了。
     *
     * 例如，在本题中，如果我们要删除节点 yy，我们需要知道节点 yy 的前驱节点 xx，并将 xx 的指针指向 yy 的后继节点。但由于头节点不存在前驱节点，因此我们需要在删除头节点时进行特殊判断。但如果我们添加了哑节点，那么头节点的前驱节点就是哑节点本身，此时我们就只需要考虑通用的情况即可。
     *
     * 特别地，在某些语言中，由于需要自行对内存进行管理。因此在实际的面试中，对于「是否需要释放被删除节点对应的空间」这一问题，我们需要和面试官进行积极的沟通以达成一致。下面的代码中默认不释放空间。
     *
     * 方法一：计算链表长度
     * 思路与算法
     *
     * 一种容易想到的方法是，我们首先从头节点开始对链表进行一次遍历，得到链表的长度 LL。随后我们再从头节点开始对链表进行一次遍历，当遍历到第 L-n+1L−n+1 个节点时，它就是我们需要删除的节点。
     *
     * 为了与题目中的 nn 保持一致，节点的编号从 11 开始，头节点为编号 11 的节点。
     *
     * 为了方便删除操作，我们可以从哑节点开始遍历 L-n+1L−n+1 个节点。当遍历到第 L-n+1L−n+1 个节点时，它的下一个节点就是我们需要删除的节点，这样我们只需要修改一次指针，就能完成删除操作。
     *
     * 复杂度分析
     *
     * 时间复杂度：O(L)O(L)，其中 LL 是链表的长度。
     *
     * 空间复杂度：O(1)O(1)。
     * 作者：LeetCode-Solution
     * 链接：https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/shan-chu-lian-biao-de-dao-shu-di-nge-jie-dian-b-61/
     */
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode tmp = new ListNode(0);
        tmp.next = head;
        ListNode p = head;
        int len = 0;
        while (p!=null){
            len++;
            p=p.next;
        }
        int k =  len-1- n;
        ListNode q=tmp;
        while (k>=0){
            q=q.next;
            k--;
        }
        q.next = q.next.next;
        return tmp.next;
    }

    /**
     * @author: shm
     * @dateTime: 2020/10/18 9:38
     * @description: 方法二：栈
     * 思路与算法
     *
     * 我们也可以在遍历链表的同时将所有节点依次入栈。根据栈「先进后出」的原则，我们弹出栈的第 nn 个节点就是需要删除的节点，并且目前栈顶的节点就是待删除节点的前驱节点。这样一来，删除操作就变得十分方便了。
     *
     * 复杂度分析
     *
     * 时间复杂度：O(L)O(L)，其中 LL 是链表的长度。
     *
     * 空间复杂度：O(L)O(L)，其中 LL 是链表的长度。主要为栈的开销。
     * 作者：LeetCode-Solution
     * 链接：https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/shan-chu-lian-biao-de-dao-shu-di-nge-jie-dian-b-61/
     */
    public ListNode removeNthFromEnd_1(ListNode head, int n) {
        ListNode tmp = new ListNode(0);
        tmp.next = head;
        ListNode p = tmp;
        Stack<ListNode> stack = new Stack<>();

        while (p!=null){
            stack.push(p);
            p = p.next;
        }

        for (int i = 0; i < n; i++) {
            stack.pop();
        }
        ListNode peek = stack.peek();
        peek.next = peek.next.next;
        return tmp.next;
    }

    public ListNode removeNthFromEnd_2(ListNode head, int n) {
        List<ListNode> listNodes = new ArrayList<>();
        ListNode p = head;
        while (p!=null) {
            listNodes.add(p);
            p = p.next;
        }
        int size = listNodes.size();
        int k = size-1 - n;
        if (k<0){
            if (size==1) {
                return null;
            }else {
                return listNodes.get(1);
            }
        }else {
            ListNode node = listNodes.get(k);
            node.next = node.next.next;
        }
        return head;
    }

    /**
     * @author: shm
     * @dateTime: 2020/10/18 9:44
     * @description: 方法三：一次遍历
     * 思路与算法
     *
     * 我们也可以在不预处理出链表的长度，以及使用常数空间的前提下解决本题。
     *
     * 由于我们需要找到倒数第 nn 个节点，因此我们可以使用两个指针 \textit{first}first 和 \textit{second}second 同时对链表进行遍历，并且 \textit{first}first 比 \textit{second}second 超前 nn 个节点。当 \textit{first}first 遍历到链表的末尾时，\textit{second}second 就恰好处于倒数第 nn 个节点。
     *
     * 具体地，初始时 \textit{first}first 和 \textit{second}second 均指向头节点。我们首先使用 \textit{first}first 对链表进行遍历，遍历的次数为 nn。此时，\textit{first}first 和 \textit{second}second 之间间隔了 n-1n−1 个节点，即 \textit{first}first 比 \textit{second}second 超前了 nn 个节点。
     *
     * 在这之后，我们同时使用 \textit{first}first 和 \textit{second}second 对链表进行遍历。当 \textit{first}first 遍历到链表的末尾（即 \textit{first}first 为空指针）时，\textit{second}second 恰好指向倒数第 nn 个节点。
     *
     * 根据方法一和方法二，如果我们能够得到的是倒数第 nn 个节点的前驱节点而不是倒数第 nn 个节点的话，删除操作会更加方便。因此我们可以考虑在初始时将 \textit{second}second 指向哑节点，其余的操作步骤不变。这样一来，当 \textit{first}first 遍历到链表的末尾时，\textit{second}second 的下一个节点就是我们需要删除的节点。
     *
     * 复杂度分析
     *
     * 时间复杂度：O(L)O(L)，其中 LL 是链表的长度。
     *
     * 空间复杂度：O(1)O(1)。
     * 作者：LeetCode-Solution
     * 链接：https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/shan-chu-lian-biao-de-dao-shu-di-nge-jie-dian-b-61/
     */
    public ListNode removeNthFromEnd_3(ListNode head, int n) {
        ListNode tmp = new ListNode(0);
        tmp.next = head;
        ListNode first = head;
        ListNode second = tmp;
        for (int i = 0; i < n; i++) {
            first = first.next;
        }
        while (first!=null){
            first = first.next;
            second=second.next;
        }
        second.next = second.next.next;
        return tmp.next;
    }

    /**
     * 虚拟头结点
     * 虚拟头结点的存在非必要，但是可以简化对链表问题的讨论，这个技巧其实我们在插入排序的时候用过，也叫 哨兵，只不过在链表中的哨兵是我们人为引入的，它不参与链表的业务，只起到占位和回避边界条件讨论的作用；
     * 在链表的实现中，我们一般也都会设置「虚拟头结点」。这是非常常见的技巧。仍然是在上面在删除倒数第 NN 个结点的任务中，如果链表删到都没有结点了，怎么还能引用到链表呢，空链表又怎么在链表的末尾添加结点呢。
     * 有了虚拟头结点以后，这道题的新的解决方案是：
     *
     * 先遍历输出链表的长度；
     * 创建虚拟头结点，接在原来链表的头部（这一步很多教科书上称为「头插法」）；
     * 然后从虚拟头结点开始遍历，走 len - N 步，这时候，就不要减 11 了，需要多走一步（因为虚拟头结点占了一个位置），同样，对走几步想不清楚的朋友，还是建议在纸上写写画画，便不难得出正确答案；
     * 注意：这里我们从虚拟头结点开始走，就是为了避免那个最极端的情况。再想一想 66 个结点，删除倒数第 66 个，从虚拟头结点开始，走 00 步，也就是原地不动，修改结点的 next 指针指向即可。
     * 复杂度分析：
     *
     * 时间复杂度：O(N)O(N)，这里 NN 是链表的结点数；
     * 空间复杂度：O(1)O(1)。
     * 注意：
     *
     * 虚拟头结点是几乎所有链表实现里的实现技巧，如果要求我们实现一个链表，一般我们都会实现带有虚拟头结点的链表，在单链表、双链表、循环链表、循环双链表中，均是如此；
     *
     * 作者：liweiwei1419
     * 链接：https://leetcode-cn.com/leetbook/read/learning-algorithms-with-leetcode/5ey1wd/
     * @param head
     * @param n
     * @return
     */
    public ListNode removeNthFromEnd_4(ListNode head, int n) {
        // 先获得链表的长度
        int len = getLenOfNode(head);

        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        ListNode curNode = dummyNode;
        // 从虚拟头结点开始走 len - n 步
        for (int i = 0; i < len - n; i++) {
            curNode = curNode.next;
        }

        // 删除结点，先声明待删除结点
        ListNode deleteNode = curNode.next;
        // 再修改指针指向
        curNode.next = deleteNode.next;
        // 删除结点的 next 指针释放引用
        deleteNode.next = null;
        return dummyNode.next;
    }


    private int getLenOfNode(ListNode head) {
        int len = 0;
        while (head != null) {
            len++;
            head = head.next;
        }
        return len;
    }
}
